-->
Kubernetes is a platform for building platforms. It’s a better place to start; not the endgame. — Kelsey Hightower
Kubernetes: The Overlord of Containers (But in a Good Way!)
Let’s be real — running containerized apps in production without Kubernetes is like herding cats. Sure, you can manage a few containers with Docker Compose, but the moment you scale up, things start slipping through the cracks. Enter Kubernetes, the ultimate traffic cop, janitor, and executive assistant for your containers.
But Kubernetes isn’t just about keeping things running — it’s about running them smartly. And when standard Kubernetes resources aren’t enough, you level up with Operators — your personal automation minions that extend Kubernetes’ brainpower to handle complex app logic.
In this guide, we’ll dive into what Kubernetes does, why it exists (hint: because ops teams like sleep), and how Operators fit into the grand scheme of things. And because we like to get our hands dirty, we’ll build our own Operator using Kubebuilder and Go. Ready? Let’s roll.
Imagine you run a taco truck. Business is booming, and now you’ve got 50 taco trucks across the city. Amazing, right? But suddenly, you have new problems:
This is exactly the kind of chaos that happens when running containerized applications at scale. And just like you’d need a fleet manager to coordinate all your taco trucks, you need Kubernetes to manage your containers.
Kubernetes makes sure:
✅ Your containers are running and healthy.
✅ New containers spin up when traffic surges.
✅ Everything is deployed and updated smoothly (no taco shortages!).
In short, Kubernetes is like your personal fleet manager — but for containers instead of tacos. 🌮
Kubernetes is your personal fleet manager — cool. But now you are staring at the command line tools, YAML files, and architecture diagrams that resemble last night’s spaghetti dinner rather than the microservices you’d like to deploy. So what now?
Using Kubernetes doesn’t mean you need a PhD in Clusterology™. Here’s the simplified game plan:
1️⃣ Tell Kubernetes what you want — You define how many taco staff (containers) you need, the truck (pod), what ingredients they should have (config + storage + secrets), the address they are at (service), and where they should be placed (scheduling). There are much more Kinds (the names in the parenthesis) you can make or define in the sprawling amount of YAML files.
2️⃣ Hand over the keys — Kubernetes takes your instructions (YAML files), does its magic, and keeps everything running. If a truck (pod) crashes? Kubernetes replaces it. Too much demand? Kubernetes scales up the trucks to meet that demand.
3️⃣ Profit. (Okay, not literally, but your app stays online, and that’s priceless.)
And the best part? You don’t have to do everything manually. Kubernetes has Operators (which we’ll dive into soon) that act like robot assistants, automating even the trickiest tasks for you.
First, we’ll use Kind — a tool for running local Kubernetes clusters inside Docker. Installing it with Go is easy:
go install sigs.k8s.io/kind@v0.26.0
Once installed, create a local cluster with:
kind create cluster
By default, this switches your kubectl context to the new Kind cluster. You can verify it’s running by checking all pods:
kubectl get pod -A
The operator pattern aims to capture the key aim of a human operator who is managing a service or set of services. Human operators who look after specific applications and services have deep knowledge of how the system ought to behave, how to deploy it, and how to react if there are problems.
People who run workloads on Kubernetes often like to use automation to take care of repeatable tasks. The operator pattern captures how you can write code to automate a task beyond what Kubernetes itself provides. — Kubernetes Docs
Operators are built using Custom Resource Definitions (CRDs) and controllers:
Operators let you codify human operational knowledge into Kubernetes, making it self-managing. Whether you’re running PostgreSQL, Kafka, or a taco stand, Operators help Kubernetes handle complex workloads autonomously.
Want to build one? Let’s roll up our sleeves and build the “Taco Operator” with Golang’s Kubebuilder!
mkdir taco-operator && cd taco-operator
kubebuilder init --domain tacos.io --repo github.com/myuser/taco-operator
This creates a Go module and sets up the necessary files for our Operator.
Since Kubernetes doesn’t natively understand tacos (yet), we need to teach it by defining a Custom Resource Definition (CRD). This tells Kubernetes what a “Taco” is and what fields it should have.
Run the following command to create a new API:
kubebuilder create api --group food --version v1alpha1 --kind TacoOrder
After saying yes to create resources and create controller, we have can customize our CRDs and implement out the new API in api/v1alpha1/tacoorder_types.go.
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// TacoOrderSpec defines the desired state of TacoOrder
type TacoOrderSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// Type of taco (e.g., al pastor, carne asada, veggie)
Type string `json:"type,omitempty"`
// Quantity of tacos to be made
Quantity int `json:"quantity,omitempty"`
// Extra toppings like guac, salsa, or cheese
Extras []string `json:"extras,omitempty"`
}
// TacoOrderStatus defines the observed state of TacoOrder
type TacoOrderStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Served int `json:"served,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// TacoOrder is the Schema for the tacoorders API
type TacoOrder struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TacoOrderSpec `json:"spec,omitempty"`
Status TacoOrderStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// TacoOrderList contains a list of TacoOrder
type TacoOrderList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TacoOrder `json:"items"`
}
func init() {
SchemeBuilder.Register(&TacoOrder{}, &TacoOrderList{})
}
Pretty nifty we only needed to change a couple of types right! Now let’s generate the CRDs and apply them to the cluster!
make generate
make manifests
kubectl apply -k config/crd/
You can confirm this is applied with:
kubectl get crd
Now that Kubernetes understands tacos, we need to write the logic that “cooks” them. This happens in the controller. 🌮
Modify internal/controller/tacoorder_controller.go to add business logic:
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
foodv1alpha1 "github.com/joshbrgs/taco-operator/api/v1alpha1"
)
// TacoOrderReconciler reconciles a TacoOrder object
type TacoOrderReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=food.tacos.io,resources=tacoorders,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=food.tacos.io,resources=tacoorders/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=food.tacos.io,resources=tacoorders/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the TacoOrder object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/reconcile
func (r *TacoOrderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
var order foodv1alpha1.TacoOrder
if err := r.Get(ctx, req.NamespacedName, &order); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// "Cooking" the tacos by updating the status
order.Status.Served = order.Spec.Quantity
fmt.Printf("🌮 Serving %d %s tacos with extras: %v\n", order.Spec.Quantity, order.Spec.Type, order.Spec.Extras)
if err := r.Status().Update(ctx, &order); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *TacoOrderReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&foodv1alpha1.TacoOrder{}).
Complete(r)
}
Now, build and run the Operator:
make docker-build IMG=my-registry/taco-operator:0.0.1
#Loads the image to your local node so you do not need a remote repo!
kind load docker-image my-registry/taco-operator:0.0.1
make deploy IMG=my-registry/taco-operator:0.0.1
kubectl get pod -A
Now for the fun part — placing a taco order using kubectl! 🎉
Create a tacoorder.yaml file:
apiVersion: food.tacos.io/v1alpha1
kind: TacoOrder
metadata:
name: my-taco-order
spec:
type: al_pastor
quantity: 3
extras:
- guacamole
- extra cheese
Now apply it:
kubectl apply -f tacoorder.yaml
Let’s take a look at the logs:
kubectl logs -l control-plane=controller-manager -n taco-operator-system
You should see something like this:
🌮 Serving 3 al_pastor tacos with extras: [guacamole extra cheese]
Boom! Kubernetes just made tacos. 🔥
To delete your CRDs from the cluster:
make uninstall
Undeploy the controller to the cluster:
make undeploy
We’ve turned our cluster into a fully operational taco stand.
But this is just the appetizer — you can expand on this by adding alerts for low taco supply, custom dashboards in Grafana for taco trends, or even AI-driven recommendations for the next taco flavor of the month.
So whether you’re running a real microservice or just having fun with Kubernetes, remember: orchestrating applications is a lot like making tacos — it’s all about balance, automation, and a little bit of spice. 🌶️
Now go forth and deploy, because the world needs more deliciously managed infrastructure!
Introduction - The Kubebuilder Book
Get exclusive discounts and notifications